﻿namespace Example.Cryptography
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Security.Cryptography;
    using System.Xml;
    using System.Xml.XPath;

    public sealed class RijndaelSettings : IDisposable
    {
        private RijndaelManaged _algorithm;

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This object disposes the algorithm.")]
        public RijndaelSettings()
            : this(new RijndaelManaged())
        {
        }

        public RijndaelSettings(string config)
            : this(ToRijndael(config))
        {
        }

        public RijndaelSettings(IXPathNavigable config)
            : this(ToRijndael(config))
        {
        }

        public RijndaelSettings(string key, 
                                string iv)
            : this(Convert.FromBase64String(key), Convert.FromBase64String(iv))
        {
        }

        public RijndaelSettings(byte[] key, 
                                byte[] iv)
            : this(ToRijndael(key, iv))
        {
        }

        public RijndaelSettings(RijndaelManaged algorithm)
        {
            Algorithm = algorithm;
        }

        ~RijndaelSettings()
        {
            Dispose(false);
        }

        private RijndaelManaged Algorithm
        {
            get
            {
                return _algorithm;
            }

            set
            {
                if (null == value)
                {
                    throw new ArgumentNullException("value");
                }

                _algorithm = value;
            }
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ciphertext", Justification = "The casing is correct.")]
        public RijndaelPlaintext Decrypt(RijndaelCiphertext ciphertext)
        {
            if (null == ciphertext)
            {
                throw new ArgumentNullException("ciphertext");
            }

            RijndaelPlaintext plaintext;

            var decryptor = Algorithm.CreateDecryptor(Algorithm.Key, Algorithm.IV);

            using (var memory = new MemoryStream(ciphertext.ToBytes()))
            {
                using (var crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
                {
                    using (var reader = new StreamReader(crypto))
                    {
                        plaintext = new RijndaelPlaintext(reader.ReadToEnd());
                    }
                }
            }

            return plaintext;
        }

        public RijndaelCiphertext Encrypt(RijndaelPlaintext plaintext)
        {
            if (null == plaintext)
            {
                throw new ArgumentNullException("plaintext");
            }

            RijndaelCiphertext ciphertext;

            var encryptor = Algorithm.CreateEncryptor(Algorithm.Key, Algorithm.IV);

            using (var memory = new MemoryStream())
            {
                using (var crypto = new CryptoStream(memory, encryptor, CryptoStreamMode.Write))
                {
                    using (var writer = new StreamWriter(crypto))
                    {
                        writer.Write(plaintext);
                    }
                }

                ciphertext = new RijndaelCiphertext(memory.ToArray());
            }

            return ciphertext;
        }

        public override string ToString()
        {
            var key = Convert.ToBase64String(Algorithm.Key);
            var iv = Convert.ToBase64String(Algorithm.IV);

            return string.Format(CultureInfo.InvariantCulture, "<rijndael key=\"{0}\" iv=\"{1}\"/>", key, iv);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private static RijndaelManaged ToRijndael(string config)
        {
            if (null == config)
            {
                throw new ArgumentNullException("config");
            }

            var xml = new XmlDocument();
            xml.LoadXml(config);

            return ToRijndael(xml.DocumentElement);
        }

        private static RijndaelManaged ToRijndael(IXPathNavigable config)
        {
            string key = null;
            string iv = null;

            if (null == config)
            {
                throw new ArgumentNullException("config");
            }

            var iterator = config.CreateNavigator().Select("/rijndael/@key");
            if (iterator.MoveNext() && null != iterator.Current)
            {
                key = iterator.Current.Value;
            }

            iterator = config.CreateNavigator().Select("/rijndael/@iv");
            if (iterator.MoveNext() && null != iterator.Current)
            {
                iv = iterator.Current.Value;
            }

            return ToRijndael(key, iv);
        }

        private static RijndaelManaged ToRijndael(string key, 
                                                  string iv)
        {
            return ToRijndael(Convert.FromBase64String(key), Convert.FromBase64String(iv));
        }

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "It is the responsibility of the calling code to dispose.")]
        private static RijndaelManaged ToRijndael(byte[] key, 
                                                  byte[] iv)
        {
            var result = new RijndaelManaged
                             {
                                 Key = key, 
                                 IV = iv
                             };

            return result;
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                Algorithm.Dispose();
            }
        }
    }
}